﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Microsoft.FxCop.Sdk;
using System.Diagnostics;

namespace Community.StaticAnalysis.Reliability.Rules
{
    /// <summary>
    /// A class that searches after disposable objects
    /// </summary>
    //HACK: the VariableList so be a Set instead of a collection. It is not allowed to have duplicates
    //in it.
    public class DiscoverDisposableObjectVisitor : BinaryReadOnlyVisitor
    {
        #region Fields

        private FinallyBlockVisitor _finallyBlockVisitor;
        private Collection<Construct> _constructList = new Collection<Construct>();
        private Collection<MethodCall> _methodCallList = new Collection<MethodCall>();

        #endregion

        #region Constructor

        /// <summary>
        /// Creates an instance of this class
        /// </summary>
        public DiscoverDisposableObjectVisitor() : this(new FinallyBlockVisitor()) { }

        /// <summary>
        /// Creates an instance of this class
        /// </summary>
        /// <param name="finallyBlockVisitor">A visitor that can inspect a finally block</param>
        public DiscoverDisposableObjectVisitor(FinallyBlockVisitor finallyBlockVisitor)
        {
            if (finallyBlockVisitor == null)
            {
                throw new ArgumentNullException("finallyBlockVisitor");
            }

            _finallyBlockVisitor = finallyBlockVisitor;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Get the variables that are not disposed correctly
        /// </summary>
        public ReadOnlyCollection<Variable> VariableList
        {
            get { return new ReadOnlyCollection<Variable>(_finallyBlockVisitor.VariableList); }
        }

        /// <summary>
        /// Returns a list with the constructor that are not correctly disposed
        /// </summary>
        public ReadOnlyCollection<Construct> ConstructList
        {
            get { return new ReadOnlyCollection<Construct>(_constructList); }
        }
        /// <summary>
        /// Returns a list with the MethodCalls that are not correctly disposed
        /// </summary>
        public ReadOnlyCollection<MethodCall> MethodCallList
        {
            get { return new ReadOnlyCollection<MethodCall>(_methodCallList); }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Visits the construct.
        /// </summary>
        /// <param name="construct">The construct.</param>
        public override void VisitConstruct(Construct construct)
        {
            base.VisitConstruct(construct);
            TypeNode type = GetReturnTypeFromConstructor(construct);
            if (type != null && Util.HasDisposableInterface(type))
            {
                SafeAddToList(_constructList, construct);
            }

            
        }

        /// <summary>
        /// Visits the method call.
        /// </summary>
        /// <param name="call">The call.</param>
        public override void VisitMethodCall(MethodCall call)
        {
            TypeNode type = GetReturnTypeFromMethod(call);
            if (type != null && Util.HasDisposableInterface(type))
            {
                SafeAddToList(_methodCallList, call);
            }

            base.VisitMethodCall(call);

        }


        /// <summary>
        /// visits that assignment statments
        /// </summary>
        /// <param name="assignment"></param>
        public override void VisitAssignmentStatement(AssignmentStatement assignment)
        {
            base.VisitAssignmentStatement(assignment);

            Construct construct = assignment.Source as Construct;
            MethodCall call = assignment.Source as MethodCall;

            //it is possible that MSIL generate a temp local for storing a result.
            //It must be checked and if true remove the source and add the target variable 
            // see the test method with the if else statement
            if (IsAssignmentBetweenTwoDisposableLocals(assignment))
            {
                SwitchVariablesInList((Variable)assignment.Source, (Variable)assignment.Target);
            }

            if (construct != null || call != null)
            {
                Variable target = null;
                TypeNode type = null;
                type = GetReturnTypeFromMethodOrConstructor(construct, call);

                target = assignment.Target as Variable;

                if (IsTargetOutParam(assignment.Target) || IsTargetRefParam(assignment.Target))
                {
                    RemoveConstructOrMethodFromLists(construct, call);
                    return;
                }

                if (type != null && Util.HasDisposableInterface(type) && target != null)
                {
                    SafeAddToList(_finallyBlockVisitor.VariableList, target);
                    RemoveConstructOrMethodFromLists(construct, call);
                }
            }

        }

        private void RemoveConstructOrMethodFromLists(Construct construct, MethodCall call)
        {
            SafeRemoveFromList(_constructList, construct);
            SafeRemoveFromList(_methodCallList, call);

        }

        private bool IsTargetReferenceParam(Expression target)
        {
            AddressDereference deReference = target as AddressDereference;
            if (deReference != null)
            {
                return true;
            }

            return false;
        }

        private void SwitchVariablesInList(Variable sourceVariable, Variable targetVariable)
        {
            _finallyBlockVisitor.VariableList.Remove(sourceVariable);
            _finallyBlockVisitor.VariableList.Add(targetVariable);

        }

        private bool IsAssignmentBetweenTwoDisposableLocals(AssignmentStatement assignment)
        {
            Variable sourceVar = assignment.Source as Variable;
            Variable targetVar = assignment.Target as Variable;

            if (sourceVar != null && targetVar != null)
            {
                if (Util.HasDisposableInterface(sourceVar.Type) && Util.HasDisposableInterface(targetVar.Type))
                {
                    return true;
                }
            }
            return false;
        }

        private bool IsTargetOutParam(Expression target)
        {
            Parameter param = target as Parameter;

            if (param != null && param.IsOut)
            {
                return true;
            }
            return false;
        }

        private bool IsTargetRefParam(Expression target) {
            AddressDereference deReference = target as AddressDereference;

            if (deReference != null) 
            {
                return true;
            }
            return false;
        }

        private TypeNode GetReturnTypeFromConstructor(Construct construct)
        {
            TypeNode type = null;
            MemberBinding binding = construct.Constructor as MemberBinding;
            if (binding != null)
            {
                type = binding.BoundMember.DeclaringType;
            }

            return type;
        }

        private TypeNode GetReturnTypeFromMethod(MethodCall call)
        {
            TypeNode type = null;

            if (call != null)
            {
                MemberBinding binding;
                binding = call.Callee as MemberBinding;
                if (binding != null)
                {
                    type = binding.Type;
                }
            }
            return type;
        }


        private TypeNode GetReturnTypeFromMethodOrConstructor(Construct construct, MethodCall call)
        {
            TypeNode type = null;
            MemberBinding binding = null;
            if (construct != null)
            {
                binding = construct.Constructor as MemberBinding;
                if (binding != null)
                {
                    type = binding.BoundMember.DeclaringType;
                }
            }
            else
            {
                if (call != null)
                {
                    binding = call.Callee as MemberBinding;
                    if (binding != null)
                    {
                        type = binding.Type;
                    }
                }
            }
            return type;
        }

        private void SafeRemoveFromList<T>(Collection<T> list, T objectToRemove) where T : class
        {
            if (objectToRemove != null)
            {
                list.Remove(objectToRemove);
            }
        }

        private void SafeAddToList<T>(Collection<T> list, T objectToAdd)
        {
            if (!list.Contains(objectToAdd))
            {
                list.Add(objectToAdd);
            }
        }


        /// <summary>
        /// Visits the return statments
        /// </summary>
        /// <param name="returnInstruction"></param>
        public override void VisitReturn(ReturnNode returnInstruction)
        {
            Variable var;
            base.VisitReturn(returnInstruction);

            

            var = returnInstruction.Expression as Variable;
            if (var != null)
            {
                if (_finallyBlockVisitor.VariableList.Contains(var))
                {
                    SafeRemoveFromList(_finallyBlockVisitor.VariableList, var);
                }
            }

            Construct construct = returnInstruction.Expression as Construct;
            MethodCall methodCall = returnInstruction.Expression as MethodCall;

            TypeNode type = GetReturnTypeFromMethodOrConstructor(construct, methodCall);

            if (type != null && Util.HasDisposableInterface(type)) {
                RemoveConstructOrMethodFromLists(construct, methodCall);
            }

            
        }

        /// <summary>
        /// Visits the finnaly clause
        /// </summary>
        /// <param name="finallyClause"></param>
        public override void VisitFinally(FinallyNode finallyClause)
        {
            base.VisitFinally(finallyClause);
            _finallyBlockVisitor.VisitFinally(finallyClause);
        }

        #endregion

    }
}